perm filename DEBUG.MEM[1,JMC]1 blob
sn#601367 filedate 1981-08-03 generic text, type T, neo UTF8
PROLOG DEBUGGING FACILITIES
PROLOG DEBUGGING FACILITIES
PROLOG DEBUGGING FACILITIES
PROLOG DEBUGGING FACILITIES
PROLOG DEBUGGING FACILITIES
Lawrence Byrd
In this paper we shall describe the new debugging facilities that are
available in the Prolog interpreter. The purpose of these facilities, as
compared with the previous versions, is to provide more information concerning
the control flow of your program, and to allow a greater choice of when this
should occur. The main features of the debugging package are as follows:
- The 'text processor' model of Prolog execution which provides a
simple way of visualising control flow, especially during
backtracking.
- The ability to selectively set spy-points, or to exaustively trace
your program.
- The wide choice of control and information options available during
debugging.
We shall now go on to describe each of these in turn.
1. Control Flow Model
1. Control Flow Model
1. Control Flow Model
1. Control Flow Model
1. Control Flow Model
During debugging the interpreter prints out a sequence of goals in various
states of instantiation in order to show where the program has reached in its
execution. However, in order to understand what is occuring it is important
to also understand when and why the interpreter prints out goals. In a normal
programming language the key points of interest are function entry and return.
But Prolog is a non-determinate language and this introduces the complexities
of backtracking. Not only are clauses entered and left but backtracking can
suddenly re-activate them in order to produce an alternative result. One of
the major confusions that novice Prolog programmers have to face is the
question of what actually occurs when a goal fails and the system suddenly
starts backtracking. I shall now describe a model of Prolog execution which
views the program control flow in terms of movement about the program text.
This model provides a basis for the debugging mechanism in the interpreter
and, it is hoped, enables the user to view the behaviour of his program in a
consistent way.
Let us look at an example Prolog procedure :
2
*-----------------------------*
CALL | | EXIT
---------> + descendant(X,Y) + --------->
| :- offspring(X,Y). |
| |
| descendant(X,Z) |
| :- offspring(X,Y), |
<--------- + descendant(Y,Z). + <---------
FAIL | | REDO
*-----------------------------*
This is part of the simple database example given in the User's Guide. The
first clause states that Y is a descendant of X if Y is an offspring of X, and
the second clause states that Z is a descendant X if Y is an offspring of X
and if Z is a descendant of Y. In the diagram I have drawn a box around the
whole procedure and added some labelled arrows which indicate the control flow
in and out of the box. There are four such arrows which we shall look at in
turn.
Call This arrow represents initial invocation of the procedure.
When a goal of the form 'descendant(X,Y)' is required to be
port
port
port
port
satisfied, control passes through the call port of the
box
box
box
box
descendant box with the intention of matching a component
clause and then satisfying any subgoals in the body of that
clause. Notice that this is independent of whether such a
match is possible; i.e. the box is called, and then such
matters are worried about. Textually we can imagine moving
our finger to the code for descendant when meeting a call to
descendant in some other part of the code.
Exit This arrow represents a successful return from the procedure.
This occurs when the initial goal has been unified with one of
the component clauses and any subgoals have been satisfied.
port box
port box
port box
port box
Control now passes out of the exit port of the descendant box.
Textually we stop following the code for descendant and go
back to the place we came from.
Redo This arrow indicates that a subsequent goal has failed and
that the system is backtracking in an attempt to find
alternatives to previous solutions. Control passes through
port box
port box
port box
port box
the redo port of the descendant box. An attempt will now be
made to resatisfy one of the component subgoals in the body of
the clause that last succeeded; or, if that fails, to
completely rematch the original goal with an alternative
clause and then try to satisfy any subgoals in the body of
backwards
backwards
backwards
backwards
this new clause. Textually we follow the code backwards up
the way we came looking for new ways of succeeding, possibly
dropping down on to another clause and following that if
3
necessary.
Fail This arrow represents a failure of the initial goal, which
might occur if no clause if matched, or if subgoals are never
satsified, or if any solution produced is always rejected by
port
port
port
port
later processing. Control now passes out of the fail port of
box
box
box
box
the descendant box and the systems continues to backtrack.
Textually we move our finger back to the code which called
this procedure and keep moving backwards up the code looking
for choice points.
In terms of this model, the information we get about the procedure box is
only the control flow through these four ports. This means that at this level
we are not concerned with which clause matches, and how any subgoals are
satisfied, but rather we only wish to know the initial goal and the final
outcome. However, it can be seen that whenever we are trying to satisfy
their
their
their
their
subgoals, what we are actually doing is passing through the ports of their
respective boxes. If we were to follow this, then we would have complete
inside
inside
inside
inside
information about the control flow inside the descendant procedure box. I
shall go on to consider an example, but first I should point out that the box
invocation box
invocation box
invocation box
invocation box
we have drawn round the procedure should really be seen as an invocation box.
That is to say, we will have a different box for each different invocation of
the procedure. Obviously, with something like a recursive procedure, we are
going to have lots of different Calls and Exits in the control flow but these
will be for different invocations. Since this might get confusing we shall
invocation box
invocation box
invocation box
invocation box
give each invocation box a unique integer identifier.
Let us now take a look at an example. I shall use the descendant procedure
we have been looking at, and we shall follow the control flow through all the
ports of this procedure and of the 'offsping' procedure. First let me
reproduce the code :
4
*------------------------------*
CALL | | EXIT
---------> + descendant(X,Y) + --------->
| |
| *-----------------* |
| :- | offspring(X,Y). | |
| *-----------------* |
| |
| descendant(X,Z) |
| |
| *-----------------* |
| :- | offspring(X,Y), | |
| *---+---------+---* |
| | | |
| *---+---------+----* |
| | descendant(Y,Z). | |
| *---+---------+----* |
| | | |
<--------- + *-> *-< + <---------
FAIL | | REDO
*------------------------------*
*----------------------------------*
CALL | | EXIT
---------> + offspring(abraham,ishmael). + --------->
| |
| offspring(abraham,isaac). |
| |
| offspring(isaac,esau). |
| |
<--------- + offspring(isaac,jacob). + <---------
FAIL | | REDO
*----------------------------------*
It is important that you try and follow the trace we are about to look at by
physically tracing along the arrows entering and leaving the above boxes.
This will show how it is possible to follow the execution in terms of up and
down movement in the program text. Backtracking is followed by moving your
up redo
up redo
up redo
up redo
finger back up the code, every goal you meet can be seen as a redo in the
exit
exit
exit
exit
trace. If this goal succeeds again then we come out at the exit port and
travel down the text as normal, reexecuting all the following goals (see the
call fail
call fail
call fail
call fail
new calls etc). If the goal fails then we come out at the fail port and keep
going backwards looking for a choice point. The fact that we are following
all the previously executed goals back the way we came, means that it is very
easy to see where the choice point is found.
The following goal is followed by a 'fail'. The purpose of this is to force
5
all possible backtracking behaviour out of the descendant procedure. The
command as a whole can therefore never succeed. However, the point of this
trace is to observe the execution flow induced by the failure of the second
goal ('fail'). Let us assume, then, that we start with the command:
*--------------------------* *--------*
| | --> | |
?- | descendant(abraham,ANS), | | fail. |
| | <-- | |
*--------------------------* *--------*
Control first passes through the call port
of descendant
(1) Call : descendant(abraham,ANS)
I have given this invocation a unique
number and I shall hang on to the original
variable names for clarity.
(2) Call : offspring(abraham,ANS)
Obviously we have matched the first clause
of the descendant procedure and this results
in the control flow passing through the call
port of offspring.
(2) Exit : offspring(abraham,ishmael)
Immediate success on the first clause and
control passes out through the exit port.
(1) Exit : descendant(abraham,ishmael)
And thus we have satisfied the first
descendant clause.
(3) Call : fail
(3) Fail : fail
6
(1) Redo : descendant(abraham,ishmael)
Then we call fail and, as might be expected
this fails !. Control passes out of the fail
port of fail and we are now backtracking,
moving in the opposite direction from before,
looking for alternatives. When we redo
descendant we are in exactly the same place as
when we left except for the direction of
movement. Hence the instantiation state of the
goal. If you wish to know the original state
of the goal, then look back to the Call port
corresponding to this invocation number.
(2) Redo : offspring(abraham,ishmael)
(2) Exit : offspring(abraham,isaac)
We redo offspring and drop down onto the
next clause thus producing an alternative
solution.
(1) Exit : descendant(abraham,isaac)
(4) Call : fail
(4) Fail : fail
(1) Redo : descendant(abraham,isaac)
Again fail causes us to reject this
solution and to start backtracking. Notice
that this was a completely new invocation of
fail.
(2) Redo : offspring(abraham,isaac)
(2) Fail : offspring(abraham,ANS)
This time, offspring cannot offer us
another match and so we continue backtracking
with control passing out through the fail
port.
(5) Call : offspring(abraham,Y)
What has happened here is that we have
dropped down onto the second descendant clause
7
and this is a completely new offspring
invocation corresponding to the first subgoal.
(5) Exit : offspring(abraham,ishmael)
(6) Call : descendant(ishmael,ANS)
This provides a solution with which we now
recursively call descendant. This gives us a
new invocation of descendant.
(7) Call : offspring(ishmael,ANS)
(7) Fail : offspring(ishmael,ANS)
(8) Call : offspring(ishmael,Y2)
(8) Fail : offspring(ishmael,Y2)
(6) Fail : descendant(ishmael,ANS)
Ishmael has no offspring (in this example),
and so the offspring subgoals in both ancestor
clauses fail thus failing the descendant goal.
(5) Redo : offspring(abraham,ishmael)
Back we go for an alternative.
(5) Exit : offspring(abraham,isaac)
(9) Call : descendant(isaac,ANS)
(10) Call : offspring(isaac,ANS)
(10) Exit : offspring(isaac,esau)
We get a new invocation of descendant and
the offspring subgoal succeeds.
(9) Exit : descendant(isaac,esau)
(1) Exit : descendant(abraham,esau)
(11) Call : fail
(11) Fail : fail
(1) Redo : descendant(isaac,esau)
(9) Redo : descendant(isaac,esau)
This provides a final solution to the
initial ancestor goal but the fail forces
backtracking again and so back we come along
8
the redo paths.
(10) Redo : offspring(isaac,esau)
(10) Exit : offspring(isaac,jacob)
(9) Exit : descendant(isaac,jacob)
(1) Exit : descendant(abraham,jacob)
The offspring subgoal has another
alternative which produces another result for
the initial descendant goal. As can be seen,
this is abraham's last possible descendant,
however there is a certain amount of work left
to be done. Let us continue to follow the
control flow as it backtracks unsucessfuly
back to the begining.
(12) Call : fail
(12) Fail : fail
(1) Redo : descendant(abraham,jacob)
(9) Redo : descendant(isaac,jacob)
(10) Redo : offspring(isaac,jacob)
(10) Fail : offspring(issac,ANS)
(13) Call : offspring(isaac,Y3)
We are now trying the second clause for
ancestor.
(13) Exit : offspring(isaac,esau)
(14) Call : descendant(esau,ANS)
Recurse again.
(15) Call : offspring(esau,ANS)
(15) Fail : offspring(esau,ANS)
(16) Call : offspring(esau,Y4)
(16) Fail : offspring(esau,Y4)
(14) Fail : descendant(esau,ANS)
(13) Redo : offspring(isaac,esau)
(13) Exit : offspring(isaac,jacob)
(17) Call : descendant(jacob,ANS)
Try jacob.
9
(18) Call : offspring(jacob,ANS)
(18) Fail : offspring(jacob,ANS)
(19) Call : offspring(jacob,Y5)
(19) Fail : offspring(jacob,Y5)
(17) Fail : descendant(jacob,ANS)
(13) Redo : offspring(isaac,jacob)
(13) Fail : offspring(isaac,Y3)
(9) Fail : descendant(isaac,ANS)
(1) Fail : descendant(abraham,ANS)
no
And that's the end of that. I hope that this exaustive example has provided
an understanding of the control flow involved in the execution of a Prolog
program. (Not that you didn't understand this already, of course). You
call
call
call
call
should have noticed that for any invocation there is always only one call and
fail redo exit
fail redo exit
fail redo exit
fail redo exit
fail, although there may be arbitrarly many redos and corresponding exits ( >=
0 ). It is the initial call which introduces the invocation and it is here
that we first see the new invocation numbers.
If you followed the steps above by tracing along the arrows linking the
boxes in the code then you will have a good idea of the sense in which this is
a 'textual processor' model of Prolog. The execution of the program can be
directly followed in the program text. (I.e. there is nothing mysterious
going on). The 'text processing' model can be viewed as a particular
traversal of the and/or tree for the program. (This is described in my paper
"Understanding the Control Flow of Prolog Programs"). We now have the basic
model with which to understand the order of the goals printed by the debugging
facilities. One question that may have occurred to you during the above
example is "Do I really have to see ALL that ?!". The answer is no, and next
three sections examine how you can control the amount of information presented
to you during debugging.
2. Control of Debugging Facilities
2. Control of Debugging Facilities
2. Control of Debugging Facilities
2. Control of Debugging Facilities
2. Control of Debugging Facilities
In order to control the degugging facilities the interpreter provides a
range of evaluable predicates which enable the user to switch debugging on and
off, to set spy-points and to specify various kinds of 'leashing'.
(Spy-points are a way of deciding to spy only on a particular procedure. You
can decide to see control pass through the various ports of such a procedure
without actually having to exaustively trace your way there. Spy-points are
similar to breakpoints in other languages).
debug
debug
debug
debug
debug This evaluable predicate switches debugging mode on. (It is
initially off). In order for the full range of control flow
information to be available it is necessary to have this on
from the start. When it is off the system does not remember
invocations that are being executed. (This is because it is
10
expensive and not required for normal running programs). You
can switch debug mode on in the middle of execution, either
from within your program or after a control-C (see later), but
information prior to this will just be unavailable.
nodebug
nodebug
nodebug
nodebug
nodebug This switches debug mode off. If there are any spy-points set
then they will be removed.
spy X
spy X
spy X
spy X
spy X This evaluable predicate sets spy-points on all the procedures
given by X. X is either a single predicate specification, or a
list of such specifications. A predicate specification is
either of the form <atom>/<Arity>, which means the procedure
with the name <atom> and an arity of <Arity> (eg member/2,
foo/0, hello/27). Or it can be of the form <atom>, which
means all the procedures with the name <atom> that currently
have clauses in the data-base, ie this may specify multiple
procedures which have the same name but different arities (eg
member, foo, hello). If you use the form <atom> but there are
no clauses for this predicate (of any Arity) then nothing will
be done. If you really want to place a spy point on a
currently non-existent procedure, then you must use the full
form <atom>/<Arity>. (You will get a warning message in this
case - just so you know whats going on). If you set some
spy-points when debug mode is off then it will be
automatically switched on for you.
nospy X
nospy X
nospy X
nospy X
nospy X This is similar to spy X except that all the procedures given
by X will have previously set spy-points removed from them.
debugging
debugging
debugging
debugging
debugging This evaluable predicate will print out information onto the
terminal showing whether debug mode is on or off and
indicating what spy-points are set. (It also specifies
leashing information (see later)).
These conventions mean that you can be in one of three states at any time.
Either 1) Debug Mode is off, or 2) Debug Mode is on although there are no
spy-points set, or 3) Debug Mode is on and there are some spy-points set.
Setting a spy-point on a procedure indicates that you wish to see all
control flow through the various ports of its invocation boxes (ie Call, Exit,
Redo and Fail - as explained above). When control passes through such a port
(of a procedure with a spy-point set on it), a message is output showing the
invocation number, port type, goal instantiation state etc. (Similar to the
example above). The user is then asked to interact in order to decide what to
do. I shall be going on to discuss the possible options available at this
point, but first I must outline the three principle actions that can be
performed.
These three actions are LEAP, CREEP and SKIP. Each of these should be
considered as a one-off command, they say "Do this now". They are NOT
switches which put you into funny modes. Whenever you are asked to interact
11
it is irrelevant which action you used the last time - this will be a fresh
decision about what you want to do now. With that in mind let's look at each
of them in turn.
Leap means "OK, Now just keep going until you find another spy-point and
wake me up then". I.e. You will see nothing until control passes through a
port of a procedure with a spy-point set. Then a message will be printed and
you will be asked to interact again. Leaping can thus be used to follow the
execution at a higher level than exaustive tracing. All you need to do is to
set spy-points on an evenly spread set of pertinent procedures, and then
follow the control flow through these by leaping from one to the other.
Creep, on the other hand, means "Hey this looks interesting, I want to see
the very next port that control passes through regardless of spy-points".
I.e. You will get a message for the next port and will be asked again to
interact there. If you keep creeping then you will get an exaustive trace of
the whole execution (both forwards and backwards). Notice that this will
often involve seeing procedures which do not have spy-points set. If you Leap
from one of these then, obviously, you won't see any more of it (unless you
creep back to it, or use some magic as described later).
Finally, Skip means "I'm not too bothered as to what happens in there, so
just do it and don't tell me anything until you get back". (Skip is only
valid for call and redo ports). I.e. You will not see anything until control
comes back to this procedure (this will either be the exit port or the fail
port). Skip is particularly useful when used at non spy-points (while
creeping) since it gaurantees that control will be returned after the
(possibly complex) execution within the box. If you skip then no message at
all will appear until control returns. This includes calls to procedures with
spy-points set; they will be masked out during the skip. There are two ways
of overriding this : there is a quasi-skip which does not ignore spy-points,
and the 't' option after a Control-C interrupt (see later) will disable the
masking. Normally, however, this masking is just what is required!
The basic idea is that, for the most part, one is relying on spy-points that
have been set in advance to capture the overall control flow (using Leap to
watch it all go by). When something goes wrong, one is then able to 'single
step' through the program (using Creep) to see what the problem is, using Skip
to dodge uninteresing bits that would take a lot of time. Since Debug Mode
will have been on all the time, all invocations (barring those that have been
cut out of the search space (using !)), will be available so that one can
Creep ('single step') anywhere; down into places one didn't look at before,
back past spy-points etc. etc. One could even start setting extra spy-points
! Spy-points can be set at any time and they will always be applicable to the
next control movement through their particular procedure. This means that not
only can spy-points be set up for future calls, but you can set them so as to
catch procedures that have already been called. (These will then catch the
Redo's and Fail's that might then occur.)
For some cases (or more likely, for those who have obsolete methods of
debugging etched into their bones), it may be preferable to start 'single
stepping' right from the begining and so one requires a way of making a Creep
12
decision at the start, before the first goal is reached. This is done using
the following evaluable predicate:
trace
trace
trace
trace
trace This switches Debug Mode on, if it is not on already, and then
makes a Creep decision so that the next port control passes
through will produce a message and you will then be asked to
interact. Note that this is a once-off decision, you will
have to call trace again if you want the same thing to happen
on your next command. This is in accordance with the meaning
of Creep given above. Once you have reached this first port,
it is then up to you what you do - Leap, Creep or Skip (or
some other fancy option as described later).
You can also call trace by responding "t" to the Control-C handler when it
prompts you after an interrupt. This gives you the ability to start debugging
at any stage by just hitting Control-C, followed by "t <CR>". Remember,
though, that if Debug Mode was not switched on before, there will be no
information about invocations previous to the interrupt.
The above discussion has assumed that whenever you receive a message
indicating that control is passing through a particular port, you always get
prompted so that you can interact. While this is true initially, is is
however possible to change it. The debugging package allows you to vary the
type of 'leashing' that occurs. If a port is leashed then you will be
prompted and execution effectively stops while you decide what to do. If a
port is unleashed then no prompt is issued (although the trace message is
Creep
Creep
Creep
Creep
still output) and execution continues as if a Creep decision had been made.
I.e. The very next port will produce a message and will then stop or continue
depending on whether it is leashed. If all ports are unleashed then
absolutely everything will appear on your screen without so much as a
by-your-leave. Leashing, however, is not applicable to spy-points.
Spy-points will always stop, print their message, and prompt for interaction
at all their ports. (This is, after all, the point of them). The following
evaluable predicate will set the leasing mode for all non spy-points:
leash(Mode)
leash(Mode)
leash(Mode)
leash(Mode)
leash(Mode) Leashing mode is set to Mode, where mode can be one of the
following:
full - prompt on call, exit, redo and fail
tight - prompt on call, redo and fail
half - prompt on call and redo
loose - prompt on call
off - never prompt
or Mode can be an integer between 0 and 15 which will set any
arbitrary combination not covered above. (Treat the integer
as a binary number, 2'abcd, where a, b, c and d give the
yes/no (1/0) decisions for the call, exit, redo and fail ports
respectively.)
13
The initial value of mode is 'half'. This provides all the detail without
continuosly prompting at every single port. If you don't like this default
then you can always reset it, of course. (You could use a prolog.ini file for
this if you always wanted it set to something else).
This completes this section, but before going on to talk about the message
output at each port and the options available when you stop, I would just
remind you that the global debugging state (which determines what goes on) can
always be discovered by using the evaluable predicate 'debugging'. In fact,
all the predicates mentioned above will output useful messages letting you
know what they are up to.
3. Format of Debugging messages
3. Format of Debugging messages
3. Format of Debugging messages
3. Format of Debugging messages
3. Format of Debugging messages
We shall now look at the exact format of the message output by the system at
a port. All trace messages are output to the terminal regardless of where the
current output is directed. (This allows you to successfully trace programs
while they are performing file I/O). The basic format is as follows:
** (23) 6 Call : foo(hello,there,←123) ?
The '**' indicates that this is a spy-point. If this port is not for a
procedure with a spy-point set, then there will be two spaces there instead.
If this port is the requested return from a Skip then the second character
becomes '>'. This gives four possible combinations.
'**' This is a spy-point.
'*>' This is a spy-point, and you also did a Skip last time you
were in this box.
' >' This is not a spy-point, but you did a Skip last time you were
in this box.
' ' This is not a spy-point.
The number in parentheses is the unique invocation identifier. This is
continuously incrementing regardless of whether or not you are actually seeing
the invocations. (That is, for as long as Debug Mode is on). This number can
be used to cross correlate the trace messages for the various ports, since it
is unique for every invocation. It will also give an indication of the number
of procedure calls made since the start of the execution. The invocation
counter starts again for every fresh execution of a command, and it is also
reset when retries (see later) are performed.
14
The number following this is the current depth. (ie the number of direct
ancestors this goal has). This is the number that used to be given by the
previous Prolog trace mechanism.
The next word specifies the particular port (ie it will be either Call,
Exit, Redo or Fail).
The goal is then printed so that you can inspect its current instantiation
state. This is done using 'print(Goal)', which is the evaluable predicate we
discussed earlier. All goals output by the tracing mechanism can thus be
pretty printed if the user desires.
The final '?' is the prompt indicating that you should type in one of the
option codes allowed (see next section). If this particular port is unleashed
then you will obviously not get this prompt since you have specified that you
do not wish to interact at this point.
I should point out before leaving this section, that not all calls to
procedures will be traced. There are a few basic procedures which have been
made invisible since it is more convenient not to trace them. These include
all primitive I/O evaluable predicates (eg get, put, read, write), all basic
control structures (eg ',', ';', '->') and all debugging control evaluable
predicates (eg debug, spy, leash, trace). This means that you will never see
messages concerning them during debugging. (This has obvious advantages !).
4. Options available during Debugging
4. Options available during Debugging
4. Options available during Debugging
4. Options available during Debugging
4. Options available during Debugging
In this section I shall describe the particular options that are available
to you when the system prompts you after printing out a debugging message.
(As mentioned earlier, all trace message go to the terminal. In a similar way
all the responses are read from the terminal and not from the current input if
it different. This allows the debugging of programs during their I/O). All
the options are simple to learn [sic] one letter mnemonics some of which can
be optionally followed by a decimal integer. They are read from the terminal
with any blanks being completely ignored up to the next terminator (as
described earlier). Upper and lower case are considered identical, and some
options only actually require the terminator. The first basic option which is
the only one you really have to remember is "h" (followed by RETURN). This
provides help in the form of a list of available options. I shall reproduce
this list and then describe each entry in turn.
<CR> creep c creep
<LF> leap l leap
<ESC> skip s skip
x back to choice point q quasi skip
r retry r <nnn> retry goal nnn
f fail f <nnn> fail goal nnn
a abort e exit from Prolog
h help p print goal
15
w write goal d display goal
g print ancestor goals g <nnn> latest nnn ancestors
@ accept command b break
n nodebug
The first three options are the basic control decisions that we discussed
earlier. Notice that as well as single character codes you can also use the
three terminators directly in order to choose one of these. This means that
these, most used, options only require single key-strokes. To recap then:
Creep forces the system to single-step to the very next port even if it is not
a spy-point. Leap allows the system to proceed until it reaches a spy-point,
and Skip masks out debugging information during the execution of a goal
reprompting when control returns. (Skip can only be used at call and redo
ports; control then returns at either the exit or fail port of the box
concerned). Quasi skip is like skip except that it does not mask out
spy-points. If there is a spy-point within the execution of the goal then
control returns at this point and any action can be performed there. The
initial skip still gaurantees an eventual return of control, though, when the
internal execution is finished. In this way, quasi-skip is like leap plus
this gaurantee.
The X option gives you the ability to quickly fail back to the last real
choice point. Normally when you creep back through redos and fails you follow
exactly the same path that you followed in order to get where you are. The
sequence of redos and fails directly mirror the original calls and exits.
This makes it very clear as to where your program is going (if you are unsure
and are trying to learn). It also makes it possible for you to reposition
yourself somewhere where you were before in order, say, to trace something you
skipped the last time through. However, when you know that something is going
to fail back to some earlier choicepoint and all you are interested in is
where this point is, then it is rather tedious to follow the execution all the
way back. The X option is only applicable at fail and redo ports; it keeps
failing until either a call port or an exit port is traversed - this will be
just after the choice point. You cannot interact during this time but the
system prints the direct path back from where you started. This will be a
sequence of fails followed by a sequence of redos (either sequence may be
empty). These are standard debugging messages (as given earlier) except that
the first two characters will be '=>' (and you will not be prompted). This
path represents the direct path up and down the and/or tree of the proof.
When you are arrive at the call (or exit) port the normal debugging action
will occur - you will be reprompted if the port is leashed or if a spy-point
is set on the procedure.
The retry and fail options are sophisticated control options which allow you
to really manhandle yourself around the execution of your program. Retry
means "Take me back to the call port of this box", this allows you to restart
this invocation when, for example you find yourself leaving with some weird
result. The state of execution is exactly the same as when you originally
called. (Unless you are misguided enough to use side effects in your program
- ie asserts etc. will remain). Retry can be used at any of the four ports
(although at the call port it is a bit of a no-op !). Fail is similar in that
16
it means "Take me to the fail port of this box". This puts your execution in
a position where it is about to backtrack out of this invocation. I.e. you
have manually failed the initial goal. Notice that with both of these
commands you will receive a message for the port you have arrived at, and if
this port is leashed, you will also be reprompted. (Last chance to change
your mind). When a retry is performed the invocation counter is reset so that
counting will continue from the current invocation number regardless of what
happened before the retry. This is in accord with the fact that you have, in
executional terms, returned to the state before anything else was called. A
message '[ retry ]' is output to give a good visual indication of where this
occured in case you are trying to follow these numbers later.
If you supply an integer after either of the retry or fail commands, then
this is taken as specifying an invocation number and the system trys to get
you to the particular port, not of this box, but of the invocation box you
have specified. It does this by continuously failing until it reaches the
right place. (Big branch points, like when a procedure has many clauses, or
when repeat is used, are cleverly jumped over). However this process is not
guaranteed !. (Oh dear). It may be the case that the invocation you are
looking for has been completely cut out of the search space by cuts ('!') in
your program (YOU should know). If this is the case then the system will end
up going back too far. When it spots this it will stop. The result of one of
these big jumps will therefore be either to get you back to the invocation you
wanted to get to, or to the first actually available invocation before this
point. When the jump is a retry, the invocation counter will be reset as
before. (ie invocation numbers will start again from the current number of
the box arrived at). A message '[ ** JUMP ** ]' is output to give a good
visual indication of what has occured.
Abort will cause an abort of this execution. All the execution states built
so far are destroyed and you are put right back at the top level of the
interpreter. (This is the standard Prolog evaluable predicate).
Exit causes an irreversible exit from the Prolog system back to the monitor
(Operating System). (This is the 'halt' evaluable predicate).
Help displays the table of options given above.
You can also Re-print, Write or Display the message you last received
(perhaps your terminal just blew up). Print will use 'print' to print the
goal (as before), but Write and Display will 'write' and 'display' the goal
(respectively). This is useful if your pretty print routines (portray) don't
print out the full goal and you suddenly get interested as to what really is
going on down in the depths of some argument. (Or maybe you have sudden
doubts about the correctness of a pretty print routine ...... well OK maybe
not).
G provides you with a list of ANCESTORS to the current goal. That is, all
goals that are hierarchically above the current goal in the proof. This is if
you like the procedure nesting, rather than the sequence of invocations that
you would pass through if you kept failing. The list is printed using 'print'
and each entry is preceded by the invocation number in parentheses followed by
17
the depth number. (This is as would be given in a trace message.) If the
invocation does not have a number (this will occur if debug mode was not
switched on until further into the execution) then this is marked by '-'. You
can always be sure of jumping to any goal in the ancestor list (by using retry
etc). If you supply an integer then only that number of ancestors will be
last
last
last
last
printed. That is to say, the last nnn ancestors will be printed counting back
from the current goal. This option does not really provide the most optimal
facility, but the ancestor list is currently the only easily available piece
of information.
The option '@' gives you the ability to call arbitrary Prolog goals. It is
a bit like a quick one-off break. The initial message '| :- ' will be output
on your terminal, and the prompt is temporarily set to '| ' (as described
earlier). This makes continuation lines look like top level, and this is more
or less what this is since the term typed in is treated as a goal. A command
is then read from the terminal and executed. The execution of this command
will be treated as a separate execution (with invocation numbers starting from
1), and Debug Mode will still be on. If you wish to single-step you will have
to use 'trace' as the first goal as explained earlier. If you switch Debug
Mode off during this execution it will none-the-less be switched back on when
you return. However, any changes to the leashing or to spy-points will remain
in effect. This means that you can use this option to switch spy-points on
and off as you progress in your debugging. Switching spy-points on will not
only trap later calls, but, providing Debug Mode has been on all the time, it
will also trap redo's and fail's further back in the execution should you ever
backtrack back to them. When the command has been executed you will be
reprompted at the port where you issued the '@'. This re-prompting always
occurs even if you have just switched off the spy-point you are at. This
gives you a free choice of how to continue (if you have just switched it off
then this is the last you will see of it of course!). These conventions are
identical to those for Break.
Break will call the normal evaluable predicate 'break', thus putting you at
interpreter top level with the execution so far sitting underneath you. When
you end the break (Control-Z) you will be reprompted at the port at which you
broke. Note that invocation numbers will start again from 1 during the break.
The new execution is completely separate from the suspended one. Debug Mode
is not switched off as you call the break, but if you do switch it off then it
will be re-switched on when you finish the break and go back to the old
execution. The conventions about spy-point re-setting and re-prompting when
the break closes are the same as for the '@' option.
Finally, Nodebug switches Debug Mode off. You thus lose information about
invocations from here on, although any previous information remains if you
manage to switch it on again later (In your program or after a Control-C).
Notice that this is the correct way to switch debugging off at a trace point.
You cannot use the '@' or 'b' options because they always restore Debug Mode
upon return.
Well that completes this outline of the options available during debugging.
I would suggest that you just play around with them in order to familiarise
youself with their ins and outs. They are all quite straight forward really.
Table of Contents
Table of Contents
Table of Contents
Table of Contents
Table of Contents
1. Control Flow Model 1
2. Control of Debugging Facilities 9
3. Format of Debugging messages 13
4. Options available during Debugging 14